package io.finch

import cats.data._
import cats.effect.{Resource, Sync}
import cats.syntax.all._
import cats.{Alternative, Applicative, ApplicativeThrow, Apply, Functor, Id, Monad, MonadThrow, ~>}
import com.twitter.finagle.http.exp.{Multipart => FinagleMultipart}
import com.twitter.finagle.http.{Cookie => FinagleCookie, Method => FinagleMethod, Request, Response}
import com.twitter.io.Buf
import io.finch.endpoint._
import io.finch.internal._
import shapeless._
import shapeless.ops.adjoin.Adjoin
import shapeless.ops.hlist.Tupler

import java.io.{File, FileInputStream, InputStream}
import scala.reflect.ClassTag

/** An `Endpoint` represents the HTTP endpoint.
  *
  * It is well known and widely adopted in Finagle that "Your Server is a Function" (i.e., `Request => Future[Response]`). In a REST/HTTP API setting this
  * function may be viewed as `Request =1=> (A =2=> Future[B]) =3=> Future[Response]`, where transformation `1` is a request decoding (deserialization),
  * transformation `2` - is a business logic and transformation `3` is - a response encoding (serialization). The only interesting part here is transformation
  * `2` (i.e., `A => Future[B]`), which represents an application business.
  *
  * An `Endpoint` transformation (`map`, `mapAsync`, etc.) encodes the business logic, while the rest of Finch ecosystem takes care about both serialization and
  * deserialization.
  *
  * A typical way to transform (or map) the `Endpoint` is to use [[internal.Mapper]]:
  *
  * {{{
  *   import io.finch._
  *
  *   case class Foo(i: Int)
  *   case class Bar(s: String)
  *
  *   val foo: Endpoint[Foo] = get("foo") { Ok(Foo(42)) }
  *   val bar: Endpoint[Bar] = get("bar" :: path[String]) { s: String => Ok(Bar(s)) }
  * }}}
  *
  * `Endpoint` s are also composable in terms of or-else combinator (known as a "space invader" operator `:+:`) that takes two `Endpoint` s and returns a
  * coproduct `Endpoint`.
  *
  * {{{
  *   import io.finch._
  *
  *   val foobar: Endpoint[Foo :+: Bar :+: CNil] = foo :+: bar
  * }}}
  *
  * An `Endpoint` might be converted into a Finagle [[com.twitter.finagle.Service]] with `Endpoint.toService` method so it can be served within Finagle HTTP.
  *
  * {{{
  *   import com.twitter.finagle.Http
  *
  *   Http.server.serve(foobar.toService)
  * }}}
  */
trait Endpoint[F[_], A] { self =>

  /** Runs this endpoint. */
  def apply(input: Input): Endpoint.Result[F, A]

  /** Maps this endpoint to the given function `A => B`. */
  final def map[B](fn: A => B)(implicit F: Monad[F]): Endpoint[F, B] =
    mapAsync(fn.andThen(F.pure[B]))

  /** Maps this endpoint to the given function `A => Future[B]`. */
  final def mapAsync[B](fn: A => F[B])(implicit F: Monad[F]): Endpoint[F, B] =
    new Endpoint[F, B] with (Output[A] => F[Output[B]]) {

      final def apply(oa: Output[A]): F[Output[B]] = oa.traverse(fn)

      final def apply(input: Input): Endpoint.Result[F, B] = self(input) match {
        case EndpointResult.Matched(rem, trc, out) =>
          EndpointResult.Matched[F, B](rem, trc, out.flatMap(this))
        case skipped: EndpointResult.NotMatched[F] => skipped
      }

      final override def toString: String = self.toString
    }

  /** Maps this endpoint to the given function `A => Output[B]`.
    */
  final def mapOutput[B](fn: A => Output[B])(implicit F: MonadThrow[F]): Endpoint[F, B] =
    mapOutputAsync(a => F.catchNonFatal(fn(a)))

  /** Maps this endpoint to the given function `A => Future[Output[B]]`.
    */
  final def mapOutputAsync[B](fn: A => F[Output[B]])(implicit F: Monad[F]): Endpoint[F, B] =
    new Endpoint[F, B] with (Output[A] => F[Output[B]]) {
      final def apply(oa: Output[A]): F[Output[B]] = oa.traverseFlatten(fn)

      final def apply(input: Input): Endpoint.Result[F, B] = self(input) match {
        case EndpointResult.Matched(rem, trc, out) =>
          EndpointResult.Matched(rem, trc, out.flatMap(this))
        case skipped: EndpointResult.NotMatched[F] => skipped
      }

      final override def toString: String = self.toString
    }

  /** Transforms this endpoint to the given function `F[Output[A]] => F[Output[B]]`.
    *
    * Might be useful to perform some extra action on the underlying `Future`. For example, time the latency of the given endpoint.
    *
    * {{{
    *   import io.finch._
    *   import com.twitter.finagle.stats._
    *
    *   def time[A](stat: Stat, e: Endpoint[A]): Endpoint[A] =
    *     e.transform(f => Stat.timeFuture(s)(f))
    * }}}
    */
  final def transformOutput[B](fn: F[Output[A]] => F[Output[B]]): Endpoint[F, B] =
    new Endpoint[F, B] {
      final def apply(input: Input): Endpoint.Result[F, B] = self(input) match {
        case EndpointResult.Matched(rem, trc, out) =>
          EndpointResult.Matched(rem, trc, fn(out))
        case skipped: EndpointResult.NotMatched[F] => skipped
      }

      final override def toString: String = self.toString
    }

  /** Transform this endpoint to the given function `F[A] => F[B]`
    */
  final def transform[B](fn: F[A] => F[B])(implicit F: Monad[F]): Endpoint[F, B] =
    new Endpoint[F, B] {
      def apply(input: Input): Endpoint.Result[F, B] =
        self(input) match {
          case EndpointResult.Matched(rem, trc, out) =>
            EndpointResult.Matched[F, B](
              rem,
              trc,
              out.flatMap { o =>
                o.traverse(a => fn(F.pure(a)))
              }
            )
          case skipped: EndpointResult.NotMatched[F] => skipped
        }

      final override def toString: String = self.toString
    }

  /** Transform effect of endpoint given natural transformation `F ~> G`
    */
  final def mapK[G[_]](nat: F ~> G): Endpoint[G, A] =
    new Endpoint[G, A] {
      def apply(input: Input): Endpoint.Result[G, A] =
        self(input) match {
          case EndpointResult.Matched(rem, trc, out) => EndpointResult.Matched(rem, trc, nat(out))
          case skipped: EndpointResult.NotMatched[F] => skipped.asInstanceOf[EndpointResult[G, A]]
        }

      final override def toString: String = self.toString
    }

  /** Returns a product of this and `other` endpoint. The resulting endpoint returns a tuple of both values.
    *
    * This combinator is an important piece for Finch's error accumulation. In its current form, `product` will accumulate Finch's own errors (i.e.,
    * [[io.finch.Error]]s) into [[io.finch.Errors]]) and will fail-fast with the first non-Finch error (just ordinary `Exception`) observed.
    */
  final def product[B](other: Endpoint[F, B])(implicit F: MonadThrow[F]): Endpoint[F, (A, B)] =
    productWith(other)(Tuple2.apply)

  /** Returns a product of this and `other` endpoint. The resulting endpoint returns a value of resulting type for product function.
    */
  final def productWith[B, O](other: Endpoint[F, B])(p: (A, B) => O)(implicit F: MonadThrow[F]): Endpoint[F, O] =
    new Endpoint[F, O] with (((Either[Throwable, Output[A]], Either[Throwable, Output[B]])) => F[Output[O]]) {
      final private[this] def collect(a: Throwable, b: Throwable): Throwable = (a, b) match {
        case (aa: Error, bb: Error)   => Errors.of(aa, bb)
        case (aa: Error, Errors(bs))  => Errors(aa +: bs)
        case (Errors(as), bb: Error)  => Errors(as :+ bb)
        case (Errors(as), Errors(bs)) => Errors(as ++ bs)
        case (_: Error, _)            => b // we fail-fast with first non-Error observed
        case (_: Errors, _)           => b // we fail-fast with first non-Error observed
        case _                        => a
      }

      final def apply(both: (Either[Throwable, Output[A]], Either[Throwable, Output[B]])): F[Output[O]] = both match {
        case (Right(oa), Right(ob)) => F.pure(oa.flatMap(a => ob.map(b => p(a, b))))
        case (Left(a), Left(b))     => F.raiseError(collect(a, b))
        case (Left(a), _)           => F.raiseError(a)
        case (_, Left(b))           => F.raiseError(b)
      }

      final def apply(input: Input): Endpoint.Result[F, O] = self(input) match {
        case a @ EndpointResult.Matched(_, _, _) =>
          other(a.rem) match {
            case b @ EndpointResult.Matched(_, _, _) =>
              EndpointResult.Matched(
                b.rem,
                a.trc.concat(b.trc),
                a.out.attempt.product(b.out.attempt).flatMap(this)
              )
            case skipped: EndpointResult.NotMatched[F] => skipped
          }
        case skipped: EndpointResult.NotMatched[F] => skipped
      }

      final override def toString: String = self.toString
    }

  /** Composes this endpoint with the given [[Endpoint]]. */
  final def ::[B](other: Endpoint[F, B])(implicit pa: PairAdjoin[B, A], F: MonadThrow[F]): Endpoint[F, pa.Out] =
    new Endpoint[F, pa.Out] with ((B, A) => pa.Out) {
      private[this] val inner = other.productWith(self)(this)
      final def apply(b: B, a: A) = pa(b, a)
      final def apply(input: Input) = inner(input)
      final override def toString = s"${other.toString} :: ${self.toString}"
    }

  /** Sequentially composes this endpoint with the given `other` endpoint. The resulting endpoint will succeed if either this or `that` endpoints are succeed.
    *
    * ==Matching Rules==
    *
    *   - if both endpoints match, the result with a shorter remainder (in terms of consumed route) is picked
    *   - if both endpoints don't match, the more specific result (explaining the reason for not matching) is picked
    */
  final def coproduct[B >: A](other: Endpoint[F, B]): Endpoint[F, B] = new Endpoint[F, B] {
    final def apply(input: Input): Endpoint.Result[F, B] = self(input) match {
      case a @ EndpointResult.Matched(_, _, _) =>
        other(input) match {
          case b @ EndpointResult.Matched(_, _, _) =>
            if (a.rem.route.length <= b.rem.route.length) a else b
          case _ => a
        }
      case a =>
        other(input) match {
          case EndpointResult.NotMatched.MethodNotAllowed(bb) =>
            a match {
              case EndpointResult.NotMatched.MethodNotAllowed(aa) =>
                EndpointResult.NotMatched.MethodNotAllowed(aa ++ bb)
              case b => b
            }
          case _: EndpointResult.NotMatched[F] => a
          case b                               => b
        }
    }

    final override def toString: String = s"(${self.toString} :+: ${other.toString})"
  }

  /** Composes this endpoint with another in such a way that coproducts are flattened. */
  final def :+:[B](that: Endpoint[F, B])(implicit a: Adjoin[B :+: A :+: CNil], F: MonadThrow[F]): Endpoint[F, a.Out] =
    that.map(x => a(Inl(x))) coproduct self.map(x => a(Inr(Inl(x))))

  /** Recovers from any exception occurred in this endpoint by creating a new endpoint that will handle any matching throwable from the underlying future. */
  final def rescue(pf: PartialFunction[Throwable, F[Output[A]]])(implicit F: ApplicativeThrow[F]): Endpoint[F, A] =
    transformOutput(_.recoverWith(pf))

  /** Recovers from any exception occurred in this endpoint by creating a new endpoint that will handle any matching throwable from the underlying future. */
  final def handle(pf: PartialFunction[Throwable, Output[A]])(implicit F: ApplicativeThrow[F]): Endpoint[F, A] =
    transformOutput(_.recover(pf))

  /** Lifts this endpoint into one that always succeeds, with [[scala.Either]] representing both success and failure cases. */
  final def attempt(implicit F: ApplicativeThrow[F]): Endpoint[F, Either[Throwable, A]] =
    new Endpoint[F, Either[Throwable, A]] with (Either[Throwable, Output[A]] => Output[Either[Throwable, A]]) {
      final def apply(toa: Either[Throwable, Output[A]]): Output[Either[Throwable, A]] = toa match {
        case Right(oo) => oo.map(Right.apply)
        case Left(t)   => Output.payload(Left(t))
      }

      final def apply(input: Input): Endpoint.Result[F, Either[Throwable, A]] = self(input) match {
        case EndpointResult.Matched(rem, trc, out) =>
          EndpointResult.Matched(rem, trc, out.attempt.map(this))
        case skipped: EndpointResult.NotMatched[F] => skipped
      }

      final override def toString: String = self.toString
    }

  /** Overrides the `toString` method on this endpoint.
    */
  final def withToString(ts: => String): Endpoint[F, A] = new Endpoint[F, A] {
    final def apply(input: Input): Endpoint.Result[F, A] = self(input)

    final override def toString: String = ts
  }
}

/** Provides extension methods for [[Endpoint]] to support coproduct and path syntax.
  */
object Endpoint extends LowPriorityEndpointInstances {

  /** Enables a very simple syntax allowing to "map" endpoints to arbitrary functions. The types are resolved at compile time and no reflection is used.
    *
    * For example:
    *
    * {{{
    *   import io.finch._
    *   import io.cats.effect.IO
    *
    *   object Mapping extends Endpoint.Module[IO] {
    *     def hello = get("hello" :: path[String]) { s: String =>
    *       Ok(s)
    *     }
    *   }
    * }}}
    */
  trait Mappable[F[_], A] extends Endpoint[F, A] {
    self =>
    final def apply(mapper: Mapper[F, A]): Endpoint[F, mapper.Out] = mapper(self)
  }

  /** An alias for [[EndpointResult]].
    */
  type Result[F[_], A] = EndpointResult[F, A]

  /** An alias for [[EndpointModule]].
    */
  type Module[F[_]] = EndpointModule[F]

  /** Representation of function `Request => F[Response]`
    */
  type Compiled[F[_]] = Kleisli[F, Request, (Trace, Either[Throwable, Response])]

  object Compiled {

    def apply[F[_]](run: Request => F[(Trace, Either[Throwable, Response])]): Endpoint.Compiled[F] =
      new Endpoint.Compiled[F](run)

  }

  implicit final class HListEndpointOps[F[_], L <: HList](val self: Endpoint[F, L]) extends AnyVal {

    /** Converts this endpoint to one that returns any type with this `HList` as its representation. */
    def as[A](implicit gen: Generic.Aux[A, L], F: Monad[F]): Endpoint[F, A] = self.map(gen.from)

    /** Converts this endpoint to one that returns a tuple with the same types as this `HList`.
      *
      * Note that this will fail at compile time if this this `HList` contains more than 22 elements.
      */
    def asTuple(implicit t: Tupler[L], F: Monad[F]): Endpoint[F, t.Out] = self.map(t(_))
  }

  /** Implicit conversion that adds convenience methods to endpoint for optional values.
    */
  implicit final class OptionEndpointOps[F[_], A](val self: Endpoint[F, Option[A]]) extends AnyVal {

    /** If endpoint is empty it will return provided default value.
      */
    def withDefault[B >: A](default: => B)(implicit F: Monad[F]): Endpoint[F, B] =
      self.map(_.getOrElse(default))

    /** If endpoint is empty it will return provided alternative.
      */
    def orElse[B >: A](alternative: => Option[B])(implicit F: Monad[F]): Endpoint[F, Option[B]] =
      self.map(_.orElse(alternative))
  }

  implicit final class ValueEndpointOps[F[_], B](val self: Endpoint[F, B]) extends AnyVal {

    /** Converts this endpoint to one that returns any type with `B :: HNil` as its representation.
      */
    def as[A](implicit gen: Generic.Aux[A, B :: HNil], F: Monad[F]): Endpoint[F, A] =
      self.map(value => gen.from(value :: HNil))
  }

  private class EndpointFunctor[F[_]: Monad] extends Functor[Endpoint[F, *]] {
    def map[A, B](fa: Endpoint[F, A])(f: A => B): Endpoint[F, B] = fa.map(f)
  }

  private class EndpointApply[F[_]: MonadThrow] extends EndpointFunctor[F] with Apply[Endpoint[F, *]] {
    def ap[A, B](ff: Endpoint[F, A => B])(fa: Endpoint[F, A]): Endpoint[F, B] = ff.productWith(fa)(_ apply _)
  }

  private class EndpointApplicative[F[_]: MonadThrow] extends EndpointApply[F] with Applicative[Endpoint[F, *]] {
    def pure[A](x: A): Endpoint[F, A] = Endpoint.const(x)
  }

  private class EndpointAlternative[F[_]: MonadThrow] extends EndpointApplicative[F] with EndpointMonoidK[F] with Alternative[Endpoint[F, *]]

  implicit def endpointAlternative[F[_]: MonadThrow]: Alternative[Endpoint[F, *]] =
    new EndpointAlternative[F]

  implicit def endpointApplicative[F[_]: MonadThrow]: Applicative[Endpoint[F, *]] =
    new EndpointApplicative[F]

  implicit def endpointFunctor[F[_]: Monad]: Functor[Endpoint[F, *]] =
    new EndpointFunctor[F]

  /** Instantiates an [[EndpointModule]] for a given effect type `F`. This is enables better type inference when constucting endpoint instances.
    *
    * For example, `lift` infer the resulting endpoint based on the argument type (string):
    *
    * {{{
    *   import io.finch._, cats.effect.IO
    *   val e = Endpoint[IO].lift("foo") // Endpoint[IO, String]
    * }}}
    */
  def apply[F[_]]: EndpointModule[F] = EndpointModule[F]

  /** Creates an empty [[Endpoint]] (an endpoint that never matches) for a given type. */
  def empty[F[_], A]: Endpoint[F, A] =
    _ => EndpointResult.NotMatched[F]

  /** Creates an [[Endpoint]] that returns an output regardless of the input. */
  def output[F[_], A](o: F[Output[A]]): Endpoint[F, A] =
    EndpointResult.Matched(_, Trace.empty, o)

  /** Creates an [[Endpoint]] that, when composed with other endpoints, doesn't change anything. */
  def zero[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] =
    output(F.pure(Output.HNil))

  /** Creates an [[Endpoint]] that always matches and returns a given value (evaluated eagerly). */
  def const[F[_], A](a: A)(implicit F: Applicative[F]): Endpoint[F, A] =
    output(F.pure(Output.payload(a)))

  /** Creates an [[Endpoint]] that always matches and returns a given value (evaluated lazily).
    *
    * This might be useful for wrapping functions returning arbitrary value within [[Endpoint]] context.
    *
    * Example: the following endpoint will recompute a random integer on each request.
    *
    * {{{
    *   val nextInt: Endpoint[Int] = Endpoint.lift(scala.util.random.nextInt)
    * }}}
    */
  def lift[F[_], A](a: => A)(implicit F: Sync[F]): Endpoint[F, A] =
    output(F.delay(Output.payload(a)))

  /** Creates an [[Endpoint]] that always matches and returns a given `F` (evaluated lazily). */
  def liftAsync[F[_], A](fa: => F[A])(implicit F: Sync[F]): Endpoint[F, A] =
    output(F.defer(fa).map(Output.payload(_)))

  /** Creates an [[Endpoint]] that always matches and returns a given `Output` (evaluated lazily). */
  def liftOutput[F[_], A](oa: => Output[A])(implicit F: Sync[F]): Endpoint[F, A] =
    output(F.delay(oa))

  /** Creates an [[Endpoint]] that always matches and returns a given `F[Output]` (evaluated lazily). */
  def liftOutputAsync[F[_], A](foa: => F[Output[A]])(implicit F: Sync[F]): Endpoint[F, A] =
    output(F.defer(foa))

  /** Creates an [[Endpoint]] from a given `InputStream`. Uses [[cats.effect.Resource]] for safer resource management
    *
    * @see
    *   [[fromFile]]
    */
  def fromInputStream[F[_]](stream: Resource[F, InputStream])(implicit F: Sync[F]): Endpoint[F, Buf] =
    new FromInputStream[F](stream)

  /** Creates an [[Endpoint]] from a given `File`. Uses [[cats.effect.Resource]] for safer resource management
    *
    * @see
    *   [[fromInputStream]]
    */
  def fromFile[F[_]](file: File)(implicit F: Sync[F]): Endpoint[F, Buf] =
    fromInputStream[F](Resource.fromAutoCloseable(F.delay(new FileInputStream(file))))

  /** Creates an [[Endpoint]] that serves an asset (static content) from a Java classpath resource, located at `path`, as a static content. The returned
    * endpoint will only match `GET` requests with path identical to asset's.
    *
    * This could be especially useful in local development, when throughput and latency matter less than quick iterations. These means, however, are not
    * recommended for production usage. Web servers (Nginx, Apache) will do much better job serving static files.
    *
    * Example project structure:
    *
    * {{{
    *   ├── scala
    *   │   └── Main.scala
    *   └── resources
    *       ├── index.html
    *       └── script.js
    * }}}
    *
    * Example bootstrap:
    *
    * {{{
    *   Bootstrap
    *     ...
    *     .serve[Text.Html](Endpoint[IO].classpathAsset("/index.html"))
    *     .serve[Application.Javascript](Endpoint[IO].classpathAsset("/script.js"))
    *     ...
    * }}}
    *
    * @see
    *   https://docs.oracle.com/javase/8/docs/technotes/guides/lang/resources.html
    */
  def classpathAsset[F[_]](path: String)(implicit F: Sync[F]): Endpoint[F, Buf] = {
    val asset = new Asset[F](path)
    val stream = fromInputStream[F](Resource.fromAutoCloseable(F.delay(getClass.getResourceAsStream(path))))
    asset :: stream
  }

  /** Creates an [[Endpoint]] that serves an asset (static content) from a filesystem, located at `path`, as a static content. The returned endpoint will only
    * match `GET` requests with path identical to asset's.
    *
    * Example bootstrap:
    *
    * {{{
    *   Bootstrap
    *     ...
    *     .serve[Text.Html](Endpoint[IO].filesystemAsset("index.html"))
    *     .serve[Application.Javascript](Endpoint[IO].filesystemAsset("script.js"))
    *     ...
    * }}}
    */
  def filesystemAsset[F[_]](path: String)(implicit F: Sync[F]): Endpoint[F, Buf] = {
    val asset = new Asset[F](path)
    val file = fromFile[F](new File(path))
    asset :: file
  }

  /** A root [[Endpoint]] that always matches and extracts the current request.
    */
  def root[F[_]](implicit F: Sync[F]): Endpoint[F, Request] =
    new Endpoint[F, Request] {
      final def apply(input: Input): Result[F, Request] =
        EndpointResult.Matched(input, Trace.empty, F.delay(Output.payload(input.request)))

      final override def toString: String = "root"
    }

  /** An [[Endpoint]] that always matches any path.
    */
  def pathAny[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] =
    new Endpoint[F, HNil] {
      final def apply(input: Input): Result[F, HNil] =
        EndpointResult.Matched(
          input.withRoute(Nil),
          Trace.fromRoute(input.route),
          F.pure(Output.HNil)
        )

      final override def toString: String = "*"
    }

  /** An [[Endpoint]] that matches an empty path.
    */
  def pathEmpty[F[_]](implicit F: Applicative[F]): Endpoint[F, HNil] =
    new Endpoint[F, HNil] {
      final def apply(input: Input): Result[F, HNil] =
        if (input.route.isEmpty)
          EndpointResult.Matched(input, Trace.empty, F.pure(Output.HNil))
        else EndpointResult.NotMatched[F]

      final override def toString: String = ""
    }

  /** A matching [[Endpoint]] that reads a value of type `A` (using the implicit [[DecodePath]] instances defined for `A`) from the current path segment.
    */
  def path[F[_]: Sync, A: DecodePath: ClassTag]: Endpoint[F, A] =
    new ExtractPath[F, A]

  /** A matching [[Endpoint]] that reads a tail value `A` (using the implicit [[DecodePath]] instances defined for `A`) from the entire path.
    */
  def paths[F[_]: Sync, A: DecodePath: ClassTag]: Endpoint[F, List[A]] =
    new ExtractPaths[F, A]

  /** An [[Endpoint]] that matches a given string.
    */
  def path[F[_]: Sync](s: String): Endpoint[F, HNil] =
    new MatchPath[F](s)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `GET` and the underlying endpoint succeeds on it.
    */
  def get[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Get, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `POST` and the underlying endpoint succeeds on it.
    */
  def post[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Post, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `PATCH` and the underlying endpoint succeeds on it.
    */
  def patch[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Patch, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `DELETE` and the underlying endpoint succeeds on it.
    */
  def delete[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Delete, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `HEAD` and the underlying endpoint succeeds on it.
    */
  def head[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Head, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `OPTIONS` and the underlying endpoint succeeds on it.
    */
  def options[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Options, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `PUT` and the underlying endpoint succeeds on it.
    */
  def put[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Put, e)

  /** A combinator that wraps the given [[Endpoint]] with additional check of the HTTP method. The resulting [[Endpoint]] succeeds on the request only if its
    * method is `TRACE` and the underlying router endpoint on it.
    */
  def trace[F[_], A](e: Endpoint[F, A]): Mappable[F, A] =
    new Method[F, A](FinagleMethod.Trace, e)

  /** An evaluating [[Endpoint]] that reads a required HTTP header `name` from the request or raises an [[Error.NotPresent]] exception when the header is
    * missing.
    */
  def header[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, A] =
    new Header[F, Id, A](name) with Header.Required[F, A]

  /** An evaluating [[Endpoint]] that reads an optional HTTP header `name` from the request into an `Option`.
    */
  def headerOption[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, Option[A]] =
    new Header[F, Option, A](name) with Header.Optional[F, A]

  /** An evaluating [[Endpoint]] that reads a binary request body, interpreted as a `Array[Byte]`, into an `Option`. The returned [[Endpoint]] only matches
    * non-chunked (non-streamed) requests.
    */
  def binaryBodyOption[F[_]: Sync]: Endpoint[F, Option[Array[Byte]]] =
    new BinaryBody[F, Option[Array[Byte]]] with FullBody.Optional[F, Array[Byte]]

  /** An evaluating [[Endpoint]] that reads a required binary request body, interpreted as an `Array[Byte]`, or throws a [[Error.NotPresent]] exception. The
    * returned [[Endpoint]] only matches non-chunked (non-streamed) requests.
    */
  def binaryBody[F[_]: Sync]: Endpoint[F, Array[Byte]] =
    new BinaryBody[F, Array[Byte]] with FullBody.Required[F, Array[Byte]]

  /** An evaluating [[Endpoint]] that reads an optional request body, interpreted as a `String`, into an `Option`. The returned [[Endpoint]] only matches
    * non-chunked (non-streamed) requests.
    */
  def stringBodyOption[F[_]: Sync]: Endpoint[F, Option[String]] =
    new StringBody[F, Option[String]] with FullBody.Optional[F, String]

  /** An evaluating [[Endpoint]] that reads the required request body, interpreted as a `String`, or throws an [[Error.NotPresent]] exception. The returned
    * [[Endpoint]] only matches non-chunked (non-streamed) requests.
    */
  def stringBody[F[_]: Sync]: Endpoint[F, String] =
    new StringBody[F, String] with FullBody.Required[F, String]

  /** An [[Endpoint]] that reads an optional request body represented as `CT` (`ContentType`) and interpreted as `A`, into an `Option`. The returned
    * [[Endpoint]] only matches non-chunked (non-streamed) requests.
    */
  def bodyOption[F[_]: Sync, A: ClassTag, CT](implicit D: Decode.Dispatchable[A, CT]): Endpoint[F, Option[A]] =
    new Body[F, A, Option[A], CT] with FullBody.Optional[F, A]

  /** An [[Endpoint]] that reads the required request body represented as `CT` (`ContentType`) and interpreted as `A`, or throws an [[Error.NotPresent]]
    * exception. The returned [[Endpoint]] only matches non-chunked (non-streamed) requests.
    */
  def body[F[_]: Sync, A: ClassTag, CT](implicit d: Decode.Dispatchable[A, CT]): Endpoint[F, A] =
    new Body[F, A, A, CT] with FullBody.Required[F, A]

  /** Alias for `body[F, A, Application.Json]`.
    */
  def jsonBody[F[_]: Sync, A: Decode.Json: ClassTag]: Endpoint[F, A] =
    body[F, A, Application.Json]

  /** Alias for `bodyOption[F, A, Application.Json]`.
    */
  def jsonBodyOption[F[_]: Sync, A: Decode.Json: ClassTag]: Endpoint[F, Option[A]] =
    bodyOption[F, A, Application.Json]

  /** Alias for `body[F, A, Text.Plain]`
    */
  def textBody[F[_]: Sync, A: Decode.Text: ClassTag]: Endpoint[F, A] =
    body[F, A, Text.Plain]

  /** Alias for `bodyOption[A, Text.Plain]`
    */
  def textBodyOption[F[_]: Sync, A: Decode.Text: ClassTag]: Endpoint[F, Option[A]] =
    bodyOption[F, A, Text.Plain]

  /** An [[Endpoint]] that matches chunked requests and lifts their content into a generic **binary** stream passed as a type parameter. This method, along with
    * other `bodyStream` endpoints, are integration points with streaming libraries such as fs2 and iteratee.
    *
    * {{{
    *   scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator
    *
    *   scala> val bin = Endpoint[IO].binaryBodyStream[Enumerator]
    *   bin: Endpoint[IO, Enumerator[IO, Array[Byte]]] = binaryBodyStream
    * }}}
    */
  def binaryBodyStream[F[_]: Sync, S[_[_], _]](implicit
      LR: LiftReader[S, F]
  ): Endpoint[F, S[F, Array[Byte]]] = new BinaryBodyStream[F, S]

  /** An [[Endpoint]] that matches chunked requests and lifts their content into a generic **string** stream passed as a type parameter. This method, along with
    * other `bodyStream` endpoints, are integration points with streaming libraries such as fs2 and iteratee.
    *
    * {{{
    *   scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator
    *
    *   scala> val bin = Endpoint[IO].stringBodyStream[Enumerator]
    *   bin: Endpoint[IO, Enumerator[IO, String]] = stringBodyStream
    * }}}
    */
  def stringBodyStream[F[_]: Sync, S[_[_], _]](implicit
      LR: LiftReader[S, F]
  ): Endpoint[F, S[F, String]] = new StringBodyStream[F, S]

  /** An [[Endpoint]] that matches chunked requests and lifts their content into a generic stream passed as a type parameter. This method, along with other
    * `bodyStream` endpoints, are integration points with streaming libraries such as fs2 and iteratee.
    *
    * When, for example, JSON library is import, this endpoint can parse an inbound JSON stream.
    *
    * {{{
    *   scala> import io.finch._, io.finch.iteratee._, cats.effect.IO, io.iteratee.Enumerator
    *
    *   scala> import io.finch.circe._, io.circe.generic.auto._
    *
    *   scala> case class Foo(s: String)
    *
    *   scala> val json = Endpoint[IO].bodyStream[Enumerator, Foo, Application.Json]
    *   bin: Endpoint[IO, Enumerator[IO, Foo]] = bodyStream
    * }}}
    */
  def bodyStream[F[_]: Sync, S[_[_], _], A, CT <: String](implicit
      LR: LiftReader[S, F],
      A: DecodeStream.Aux[S, F, A, CT]
  ): Endpoint[F, S[F, A]] = new BodyStream[F, S, A, CT]

  /** See [[bodyStream]]. This is just an alias for `bodyStream[?, ?, Application.Json]`.
    */
  def jsonBodyStream[F[_]: Sync, S[_[_], _], A](implicit
      LR: LiftReader[S, F],
      A: DecodeStream.Aux[S, F, A, Application.Json]
  ): Endpoint[F, S[F, A]] = bodyStream[F, S, A, Application.Json]

  /** See [[bodyStream]]. This is just an alias for `bodyStream[?, ?, Text.Plain]`.
    */
  def textBodyStream[F[_]: Sync, S[_[_], _], A](implicit
      LR: LiftReader[S, F],
      A: DecodeStream.Aux[S, F, A, Text.Plain]
  ): Endpoint[F, S[F, A]] = bodyStream[F, S, A, Text.Plain]

  /** An evaluating [[Endpoint]] that reads an optional HTTP cookie from the request into an `Option`.
    */
  def cookieOption[F[_]: Sync](name: String): Endpoint[F, Option[FinagleCookie]] =
    new Cookie[F, Option[FinagleCookie]](name) with Cookie.Optional[F]

  /** An evaluating [[Endpoint]] that reads a required cookie from the request or raises an [[Error.NotPresent]] exception when the cookie is missing.
    */
  def cookie[F[_]: Sync](name: String): Endpoint[F, FinagleCookie] =
    new Cookie[F, FinagleCookie](name) with Cookie.Required[F]

  /** An evaluating [[Endpoint]] that reads an optional query-string param `name` from the request into an `Option`.
    */
  def paramOption[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, Option[A]] =
    new Param[F, Option, A](name) with Param.Optional[F, A]

  /** An evaluating [[Endpoint]] that reads a required query-string param `name` from the request or raises an [[Error.NotPresent]] exception when the param is
    * missing; an [[Error.ParamNotParsed]] exception when the param cannot be decoded.
    */
  def param[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, A] =
    new Param[F, Id, A](name) with Param.Required[F, A]

  /** An evaluating [[Endpoint]] that reads an optional (in a meaning that a resulting `Seq` may be empty) multi-value query-string param `name` from the
    * request into a `Seq`.
    */
  def params[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, List[A]] =
    new Params[F, List, A](name) with Params.AllowEmpty[F, A]

  /** An evaluating [[Endpoint]] that reads a required multi-value query-string param `name` from the request into a `NonEmptyList` or raises a
    * [[Error.NotPresent]] exception when the params are missing or empty.
    */
  def paramsNel[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, NonEmptyList[A]] =
    new Params[F, NonEmptyList, A](name) with Params.NonEmpty[F, A]

  /** An evaluating [[Endpoint]] that reads an optional file upload from a `multipart/form-data` request into an `Option`.
    */
  def multipartFileUploadOption[F[_]: Sync](name: String): Endpoint[F, Option[FinagleMultipart.FileUpload]] =
    new FileUpload[F, Option](name) with FileUpload.Optional[F]

  /** An evaluating [[Endpoint]] that reads a required file upload from a `multipart/form-data` request.
    */
  def multipartFileUpload[F[_]: Sync](name: String): Endpoint[F, FinagleMultipart.FileUpload] =
    new FileUpload[F, Id](name) with FileUpload.Required[F]

  /** An evaluating [[Endpoint]] that optionally reads multiple file uploads from a `multipart/form-data` request.
    */
  def multipartFileUploads[F[_]: Sync](name: String): Endpoint[F, List[FinagleMultipart.FileUpload]] =
    new FileUpload[F, List](name) with FileUpload.AllowEmpty[F]

  /** An evaluating [[Endpoint]] that requires multiple file uploads from a `multipart/form-data` request.
    */
  def multipartFileUploadsNel[F[_]: Sync](name: String): Endpoint[F, NonEmptyList[FinagleMultipart.FileUpload]] =
    new FileUpload[F, NonEmptyList](name) with FileUpload.NonEmpty[F]

  /** An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` request.
    */
  def multipartAttribute[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, A] =
    new Attribute[F, Id, A](name) with Attribute.Required[F, A] with Attribute.SingleError[F, Id, A]

  /** An evaluating [[Endpoint]] that reads an optional attribute from a `multipart/form-data` request.
    */
  def multipartAttributeOption[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, Option[A]] =
    new Attribute[F, Option, A](name) with Attribute.Optional[F, A] with Attribute.SingleError[F, Option, A]

  /** An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` request.
    */
  def multipartAttributes[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, List[A]] =
    new Attribute[F, List, A](name) with Attribute.AllowEmpty[F, A] with Attribute.MultipleErrors[F, List, A]

  /** An evaluating [[Endpoint]] that reads a required attribute from a `multipart/form-data` request.
    */
  def multipartAttributesNel[F[_]: Sync, A: DecodeEntity: ClassTag](name: String): Endpoint[F, NonEmptyList[A]] =
    new Attribute[F, NonEmptyList, A](name) with Attribute.NonEmpty[F, A] with Attribute.MultipleErrors[F, NonEmptyList, A]

  /** Sequentially composes the given `endpoints` by using [[Endpoint!.coproduct]].
    *
    * The resulting endpoint will match if at least one of the provided endpoints matches. If the sequence of provided endpoints is empty, the empty endpoint is
    * returned, which never matches.
    *
    * @see
    *   [[Endpoint!.coproduct]] for the exact composition semantics.
    * @see
    *   [[Endpoint.empty]] for the semantics of the empty endpoint.
    */
  def coproductAll[F[_], A](endpoints: Endpoint[F, A]*): Endpoint[F, A] =
    if (endpoints.isEmpty) empty else endpoints.reduce(_ coproduct _)
}
